Skip to content

plans: annual pricing tiers (hobby_yearly / pro_yearly / team_yearly) + billing.go plan_frequency (P2)#44

Merged
mastermanas805 merged 1 commit into
masterfrom
pricing/p2-annual-server-fresh
May 12, 2026
Merged

plans: annual pricing tiers (hobby_yearly / pro_yearly / team_yearly) + billing.go plan_frequency (P2)#44
mastermanas805 merged 1 commit into
masterfrom
pricing/p2-annual-server-fresh

Conversation

@mastermanas805
Copy link
Copy Markdown
Member

Summary

  • Adds three annual pricing variants to plans.yaml: hobby_yearly ($90/yr ≈ $7.50/mo, saves $18/yr vs $9 × 12), pro_yearly ($490/yr ≈ $40.83/mo, saves $98/yr vs $49 × 12), team_yearly ($1990/yr ≈ $165.83/mo, saves $398/yr vs $199 × 12). Limits + features mirror the monthly counterpart byte-for-byte; only price and billing_period: yearly differ.
  • Threads plan_frequency ("monthly" | "yearly", default monthly) through POST /api/v1/billing/checkout. Empty stays back-compat. Yearly with no RAZORPAY_PLAN_ID_*_YEARLY set returns 503 billing_not_configured so partial rollout is safe.
  • Webhook handler unchanged in shape — planIDToTier() recognises both cycles' plan_ids and maps them back to the canonical tier so teams.plan_tier stays cycle-agnostic.

⚠️ Operator action required

Until the operator creates the three Razorpay yearly plans in the Razorpay dashboard and sets the following keys on the instant-secrets k8s Secret, the yearly toggle returns 503:

  • RAZORPAY_PLAN_ID_HOBBY_YEARLY
  • RAZORPAY_PLAN_ID_PRO_YEARLY
  • RAZORPAY_PLAN_ID_TEAM_YEARLY

Monthly pricing is unaffected by this rollout — existing RAZORPAY_PLAN_ID_HOBBY / _PRO / _TEAM keys keep working.

Dependency

Requires InstaNode-dev/common#6 (pricing/p2-annual-plans) — adds the BillingPeriod Plan field, Registry.BillingPeriod method, CanonicalTier helper, and the three yearly entries in the embedded defaultYAML. Local builds work via go.mod replace ../common, but the common PR must merge first for CI to be green on a fresh tree.

Test plan

  • make test-unit — all packages green (incl. 22.9s handler suite)
  • 5 new billing-handler tests: `invalid_frequency` 400, yearly-unconfigured 503, monthly default with no field, team-tier guard fires on both cycles, webhook plan_id → canonical tier mapping for all 6 plan_ids
  • 3 new plans tests: `*_yearly` mirrors monthly limits + features, CanonicalTier strips suffix, base-tier behaviour unchanged
  • go build ./... and go vet ./... clean
  • Manual smoke after operator sets the three env vars: hit /api/v1/billing/checkout with {plan: pro, plan_frequency: yearly} and verify subscription opens at the yearly rate

🤖 Generated with Claude Code

… + billing.go plan_frequency (P2)

Adds the three yearly plan variants to plans.yaml and threads a new
plan_frequency field through POST /api/v1/billing/checkout. Limits +
features on each *_yearly plan mirror the monthly counterpart byte-for-byte;
only price (annual amount in cents) and billing_period: yearly differ.

## Pricing
  hobby_yearly: $90/yr   (~$7.50/mo,  saves $18/yr  vs $9  x 12)
  pro_yearly:   $490/yr  (~$40.83/mo, saves $98/yr  vs $49 x 12)
  team_yearly:  $1990/yr (~$165.83/mo, saves $398/yr vs $199 x 12)

## API changes
- `checkoutRequest.PlanFrequency` ("monthly" | "yearly", default monthly).
  Empty stays back-compat with pre-P2 dashboard clients.
- `razorpayPlanIDFor(tier, frequency)` resolves the right plan_id from
  config. Yearly env vars empty -> 503 billing_not_configured (so partial
  rollout — monthly live, yearly plans not yet created on Razorpay — is
  safe).
- `planIDToTier()` recognises both monthly and yearly plan_ids and maps
  them back to the canonical (bare) tier. teams.plan_tier always stores
  the canonical name so limits resolution is cycle-agnostic.
- Webhook unchanged in shape — the upgrade tier comes from the resolved
  canonical tier regardless of which cycle paid.
- OpenAPI schema documents plan_frequency on /api/v1/billing/checkout.

## New env vars (operator action — see PR body callout)
  RAZORPAY_PLAN_ID_HOBBY_YEARLY
  RAZORPAY_PLAN_ID_PRO_YEARLY
  RAZORPAY_PLAN_ID_TEAM_YEARLY

## Tests
- Unit: 5 new handler tests (invalid_frequency 400, yearly unconfigured
  503, monthly default behaviour, team-tier guard fires on both cycles,
  webhook plan_id -> canonical tier mapping for all 6 plan_ids).
- Unit: 3 new plans tests (yearly variants mirror monthly, CanonicalTier
  helper, base-tier limits unchanged).
- `make test-unit`: green across all packages.

## Dependency
Requires `InstaNode-dev/common#pricing/p2-annual-plans` (PR #6) — adds
the `BillingPeriod` Plan field, the `Registry.BillingPeriod` method,
the `CanonicalTier` helper, and the three yearly entries in the
embedded defaultYAML. This api branch's `go.mod` `replace` directive
points to `../common` so local builds work, but the merged base in
`common` must be cut before this PR lands.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant